#include <map>
#include <string>
#include <iterator>
#include <iostream>

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>

#include <openssl/ssl.h>
#include <openssl/err.h>

#include "helper.hpp"
#include "interface.hpp"
#include "tcp_server.hpp"

/* Global SSL CTX object */
SSL_CTX *g_ssl_ctlx;

static int server_sock = -1;
static int port_number = 2325;
static pthread_t tcp_client_thread;

static void* tcp_client_io_handler(void* obj)
{   
    int n, sd;
    std::string gid;
    SSL *ssl = (SSL *)obj;
    char read_buffer[4096];

    /* Get socket connection */
    sd = SSL_get_fd(ssl);


    while(true) {
        std::string response;
        bzero(read_buffer,sizeof(read_buffer));
        n = SSL_read(ssl, read_buffer, sizeof(read_buffer));
        if (n <= 0){
            logger(SM_LOG_INFO,"Connection closed by peer.");
            goto exit;
        }
        logger(SM_LOG_INFO,"REQUEST: %s", read_buffer);
        response = handle_request(read_buffer);

        /* +1 for null terminated character */
        n = SSL_write(ssl, response.c_str(), response.length() + 1); 
        if (n < 0){
            perror("ERROR writing to socket");
            pthread_exit(NULL);
        }
        logger(SM_LOG_INFO,"RESPONSE: %s", response.c_str());
    }

exit:
    close(sd);
    SSL_free(ssl);
    pthread_exit(NULL);
}

static void* tcp_client_handler(void* obj)
{   
    int client_socket;
    struct sockaddr_in client_addr;
    socklen_t client_len;

    client_len = sizeof(client_addr);
    std::vector<pthread_t*> clients_thread;

    while(true) {
        SSL *ssl;
        client_socket = accept(server_sock, (struct sockaddr *) &client_addr, &client_len);
        if (client_socket < 0){
            perror("ERROR on accepting incoming connection.");
            continue;
        }

        /* Get new SSL state with context */
        ssl = SSL_new(g_ssl_ctlx);

        /* Set connection socket to SSL state */
        SSL_set_fd(ssl, client_socket);

        /* Do SSL-protocol accept */
        if ( SSL_accept(ssl) != 1 ) {
            logger(SM_LOG_ERR, "ERROR: CREATING NEW TCP CONNECTION. !!! ACCESS DENIED !!!. "
                    "CONNECTION FROM %s:%d", inet_ntoa(client_addr.sin_addr), 
                    ntohs(client_addr.sin_port));
            /* Release SSL state */
            SSL_free(ssl);
            close(client_socket);
            continue;
        }

        logger(SM_LOG_INFO,"CONNECTED FROM %s:%d",
                inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));

        pthread_t *thread = (pthread_t*)malloc(sizeof(pthread_t));
        if(!thread){
            logger(SM_LOG_ERR, "ERROR CREATING NEW TCP CONNECTION, CAN'T ALLOCATE MEMORY.");
            continue;
        }

        for (unsigned int i = 0; i < clients_thread.size(); ++i) {
            if(!pthread_tryjoin_np(*clients_thread[i], NULL)) { // TODO
                if(clients_thread[i]){
                    free(clients_thread[i]);
                }
                clients_thread.erase(clients_thread.begin() +i);
                i = 0;
            }
        }

        clients_thread.push_back(thread);
        pthread_create(thread, NULL, &tcp_client_io_handler, (void*)ssl);
    }

    pthread_exit(NULL);
}

void ssl_ctx_init(void)
{   
    const SSL_METHOD *method;

    /* Load & register all cryptos, etc. */
    OpenSSL_add_all_algorithms();  

    /* Load all error messages */
    SSL_load_error_strings();   

    /* Create new server-method instance */
    method = SSLv23_server_method();  

    /* Create new context from method */
    g_ssl_ctlx = SSL_CTX_new(method);   
    if (g_ssl_ctlx == NULL) {
        ERR_print_errors_fp(stderr);
        abort();
    }

    /* Set the local certificate from CertFile */
    if ( SSL_CTX_use_certificate_file(g_ssl_ctlx, "server.crt", SSL_FILETYPE_PEM) <= 0 ) {
        ERR_print_errors_fp(stderr);
        abort();
    }

    /* Set the private key from KeyFile (may be the same as CertFile) */
    if ( SSL_CTX_use_PrivateKey_file(g_ssl_ctlx, "server.key", SSL_FILETYPE_PEM) <= 0 ) {
        ERR_print_errors_fp(stderr);
        abort();
    }

    /* Verify private key */
    if (!SSL_CTX_check_private_key(g_ssl_ctlx)) {
        fprintf(stderr, "Private key does not match the public certificate\n");
        abort();
    }

    /* Load  */
    if(!(SSL_CTX_load_verify_locations(g_ssl_ctlx, "smarthomeCA.pem", 0))) {
        ERR_print_errors_fp(stderr);
        abort();
    }

    SSL_CTX_set_verify(g_ssl_ctlx, SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT, NULL);
}

void init_tcp_server()
{
    int reuse = 1;
    struct sockaddr_in serv_addr;

    /* initialize SSL*/
    SSL_library_init();
    ssl_ctx_init(); 

    server_sock = socket(AF_INET, SOCK_STREAM, 0);
    if (server_sock  < 0){
        perror("ERROR opening socket");
        exit(1);
    }

    bzero((char *) &serv_addr, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = INADDR_ANY;
    serv_addr.sin_port = htons(port_number);
    if(setsockopt(server_sock, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse)) == -1){
        perror("SO_REUSEADDR");
        exit(1);
    }

    if(setsockopt(server_sock, SOL_SOCKET, SO_REUSEPORT, &reuse, sizeof(reuse)) == -1){
        perror("SO_REUSEPORT");
        exit(1);
    }

    if (bind(server_sock, (struct sockaddr *) &serv_addr, sizeof(serv_addr)) < 0){
        perror("bind");
        exit(1);
    }

    listen(server_sock, 100);
    pthread_create(&tcp_client_thread, NULL, &tcp_client_handler, NULL);
}
